#!/usr/bin/env python3
"""
Webhook_receiver-beta1.0
"""

import os
import logging
import tempfile
import shutil
import subprocess
import sys
import requests
import stat
import time
from flask import Flask, request

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
app = Flask(__name__)

# ---------- CONFIGURE via ENV ----------
GITCODE_PAT = os.environ.get("GITCODE_PAT", "your robot's gitcode pat")
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "your webhook secret")
GITCODE_API_BASE = os.environ.get("GITCODE_API_BASE", "https://api.gitcode.com")
AGENT_ROOT = os.path.abspath(os.path.dirname(__file__))
CLI_PATH = os.path.join(AGENT_ROOT, "cli.py")
WORK_DIR = os.environ.get("WORK_DIR", "/tmp/testagent_workspace")
MAVEN_HOME = os.environ.get("MAVEN_HOME", "")
JAVA_HOME = os.environ.get("JAVA_HOME", "")
JAVA_VERSION = os.environ.get("JAVA_VERSION", "")
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
NEO4J_PW = os.environ.get("NEO4J_PW", "password")

TRIGGER_KEYWORD = os.environ.get("TRIGGER_KEYWORD", "@testagent run")

# 保证工作目录存在
os.makedirs(WORK_DIR, exist_ok=True)


def validate_environment():
    """验证必要的环境变量"""
    issues = []

    if not GITCODE_PAT:
        issues.append("GITCODE_PAT is not set")

    if not WEBHOOK_SECRET:
        issues.append("WEBHOOK_SECRET is not set (ignoring signature verification)")

    if issues:
        logging.warning("Environment configuration issues:")
        for issue in issues:
            logging.warning(f"  - {issue}")
    else:
        logging.info("✅ Environment configuration looks good")


def test_gitcode_api_access():
    """测试 GitCode API 访问权限"""
    if not GITCODE_PAT:
        logging.warning("No GITCODE_PAT available for testing")
        return False

    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'private-token': GITCODE_PAT
    }

    # 测试获取用户信息
    try:
        resp = requests.get(f"{GITCODE_API_BASE}/api/v5/user", headers=headers, timeout=10)
        if resp.status_code == 200:
            user_info = resp.json()
            logging.info(f"✅ GitCode API access successful - User: {user_info.get('login', 'Unknown')}")
            return True
        else:
            logging.warning(f"GitCode API access test failed: {resp.status_code} - {resp.text}")
            return True
    except Exception as e:
        logging.warning(f"GitCode API test exception: {str(e)}")
        return True


# 初始化时验证环境
validate_environment()
test_gitcode_api_access()


def check_neo4j_connection():
    """检查Neo4j连接"""
    try:
        import neo4j
        driver = neo4j.GraphDatabase.driver(
            NEO4J_URI,
            auth=(NEO4J_USER, NEO4J_PW),
            connection_timeout=5
        )
        with driver.session() as session:
            result = session.run("RETURN 1 as test")
            value = result.single()["test"]
            logging.info(f"✅ Neo4j connection successful")
            driver.close()
            return True
    except Exception as e:
        logging.error(f"❌ Neo4j connection failed: {str(e)}")
        return False


def remove_readonly(func, path, excinfo):
    """处理Windows只读文件删除"""
    try:
        os.chmod(path, stat.S_IWRITE)
        func(path)
    except Exception as e:
        logging.warning(f"Failed to remove {path}: {str(e)}")


def safe_rmtree(path, max_retries=3):
    """安全删除目录，支持重试"""
    for i in range(max_retries):
        try:
            shutil.rmtree(path, onerror=remove_readonly)
            logging.info(f"Successfully removed directory: {path}")
            return True
        except Exception as e:
            logging.warning(f"Error removing directory (attempt {i + 1}/{max_retries}): {str(e)}")
            time.sleep(1)

    logging.error(f"Failed to remove directory after {max_retries} attempts: {path}")
    return False


def comment_on_issue(owner, repo_name, issue_number, body, data=None):
    """使用 GitCode API 在 Issue 中发布评论"""
    if not GITCODE_PAT:
        logging.error("GITCODE_PAT is not set, cannot comment on issue")
        return False

    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'private-token': GITCODE_PAT
    }

    # GitCode Issue 评论 API
    api_url = f"{GITCODE_API_BASE}/api/v5/repos/{owner}/{repo_name}/issues/{issue_number}/comments"

    payload = {
        "body": body
    }

    logging.info(f"Posting comment to GitCode API: {api_url}")

    try:
        resp = requests.post(api_url, headers=headers, json=payload, timeout=30)
        if resp.status_code in (200, 201):
            logging.info(f"✅ Comment posted successfully to issue #{issue_number}")
            return True
        else:
            logging.error(f"GitCode API failed: {resp.status_code} - {resp.text}")
            return False
    except Exception as e:
        logging.error(f"GitCode API exception: {str(e)}")
        return False


def create_pr_with_test_cases(owner, repo_name, tmpdir, issue_number, data):
    """
    创建包含测试用例的PR
    """
    if not GITCODE_PAT:
        logging.warning("GITCODE_PAT not set, skipping PR creation")
        return None

    try:
        # 配置git用户信息
        subprocess.run(["git", "config", "user.email", "testagent@example.com"],
                       cwd=tmpdir, check=True, capture_output=True)
        subprocess.run(["git", "config", "user.name", "TestAgent"],
                       cwd=tmpdir, check=True, capture_output=True)

        # 切换到新分支
        branch_name = f"testagent/generated-tests-{issue_number}"
        subprocess.run(["git", "checkout", "-b", branch_name], cwd=tmpdir, check=True, capture_output=True)

        # 复制测试用例文件到仓库中
        test_case_source = os.path.join(AGENT_ROOT, "result", "log", "test_case.jsonl")
        test_case_dest = os.path.join(tmpdir, "testagent_results", "test_case.jsonl")

        if os.path.exists(test_case_source):
            os.makedirs(os.path.dirname(test_case_dest), exist_ok=True)
            shutil.copy2(test_case_source, test_case_dest)
            logging.info(f"Copied test cases to {test_case_dest}")
        else:
            logging.warning("Test case file not found, creating placeholder")
            os.makedirs(os.path.dirname(test_case_dest), exist_ok=True)
            with open(test_case_dest, 'w', encoding='utf-8') as f:
                f.write('{"message": "No test cases were generated in this execution"}\n')

        # 添加生成的文件
        subprocess.run(["git", "add", "."], cwd=tmpdir, check=True, capture_output=True)

        # 检查是否有变更
        status_result = subprocess.run(
            ["git", "status", "--porcelain"],
            cwd=tmpdir,
            capture_output=True,
            text=True
        )

        if not status_result.stdout.strip():
            logging.info("No changes to commit")
            return None

        # 提交
        commit_result = subprocess.run(
            ["git", "commit", "-m", f"Add generated test cases by TestAgent\n\nRelated to issue #{issue_number}"],
            cwd=tmpdir,
            capture_output=True,
            text=True
        )

        # 推送到远程
        push_result = subprocess.run(
            ["git", "push", "origin", branch_name],
            cwd=tmpdir,
            capture_output=True,
            text=True
        )

        # 创建 PR
        headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'private-token': GITCODE_PAT
        }

        api_url = f"{GITCODE_API_BASE}/api/v5/repos/{owner}/{repo_name}/pulls"

        # 尝试不同的目标分支
        target_branches = ["main", "master"]

        for target_branch in target_branches:
            payload = {
                "title": f"TestAgent: 自动生成的测试用例 (Issue #{issue_number})",
                "head": branch_name,
                "base": target_branch,
                "body": f"由 TestAgent 自动生成的测试用例\n\n关联Issue: #{issue_number}\n\n测试用例文件位置: `testagent_results/test_case.jsonl`",
                "prune_source_branch": True
            }

            try:
                resp = requests.post(api_url, headers=headers, json=payload, timeout=30)
                if resp.status_code in (200, 201):
                    pr_data = resp.json()
                    # 尝试获取PR URL的不同字段
                    pr_url = pr_data.get('html_url') or pr_data.get('url') or pr_data.get('web_url')
                    logging.info(f"✅ PR created: {pr_url}")
                    return {
                        'url': pr_url,
                        'test_case_path': 'testagent_results/test_case.jsonl',
                        'branch': branch_name
                    }
                elif resp.status_code == 422:
                    # 可能是目标分支不存在，尝试下一个
                    logging.warning(f"Target branch {target_branch} may not exist, trying next")
                    continue
                else:
                    logging.error(f"PR creation failed: {resp.status_code} {resp.text}")
            except Exception as e:
                logging.error(f"PR creation exception: {str(e)}")

    except Exception as e:
        logging.error(f"PR creation failed: {str(e)}")

    return None


@app.route("/health", methods=["GET"])
def health():
    return "ok", 200


@app.route("/webhook", methods=["POST"])
def webhook():
    if WEBHOOK_SECRET:
        # GitCode 暂无签名头，跳过
        pass
    else:
        logging.warning("No WEBHOOK_SECRET set — skipping signature verification")

    try:
        data = request.get_json(force=True)
    except Exception:
        logging.exception("Invalid JSON payload")
        return "Invalid JSON", 400

    event = request.headers.get("X-GitCode-Event", "Unknown")
    logging.info(f"Received event {event}")

    triggered = False
    owner = None
    repo_name = None
    issue_number = None

    # --- GitCode Issue Hook ---
    # 在触发检测部分添加更详细的调试信息
    # 在触发检测部分修改为更灵活的关键词匹配
    if data.get("object_kind") == "issue" or data.get("event_type") == "issue":
        attrs = data.get("object_attributes", {})
        desc = attrs.get("description") or ""
        title = attrs.get("title") or ""
        full_text = desc + " " + title
        full_text_lower = full_text.lower()
        action = attrs.get("action", "")
        project = data.get("project", {})

        repo_name = project.get("name")
        namespace = project.get("namespace")
        if project.get("path_with_namespace"):
            parts = project["path_with_namespace"].split("/")
            if len(parts) >= 2:
                owner = parts[0]
                repo_name = parts[1]

        issue_number = attrs.get("iid") or attrs.get("id")
        logging.info(f"GitCode Issue Hook: {owner}/{repo_name} issue #{issue_number} action={action}")

        # 详细的调试信息
        logging.info(f"Issue title: '{title}'")
        logging.info(f"Issue description: '{desc}'")
        logging.info(f"Full text (lowercase): '{full_text_lower}'")

        # 更灵活的关键词匹配
        # 1. 首先尝试直接匹配
        # 2. 然后尝试匹配去除Markdown链接后的文本
        # 3. 最后尝试匹配只包含testagent和run的变体

        # 方法1: 直接匹配完整关键词
        keyword_direct = TRIGGER_KEYWORD.lower()
        found_direct = keyword_direct in full_text_lower

        # 方法2: 匹配去除Markdown链接后的文本
        import re
        # 移除Markdown链接，只保留链接文本
        text_without_links = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', full_text_lower)
        found_without_links = keyword_direct in text_without_links

        # 方法3: 匹配变体（只包含testagent和run）
        keyword_variant = "testagent run"
        found_variant = keyword_variant in full_text_lower or keyword_variant in text_without_links

        logging.info(f"Looking for trigger keyword: '{keyword_direct}'")
        logging.info(f"Direct match: {found_direct}")
        logging.info(f"Without links match: {found_without_links}")
        logging.info(f"Variant match: {found_variant}")

        # 任何一种匹配方式成功都触发
        triggered = found_direct or found_without_links or found_variant

        # 🟡 跳过关闭事件
        if action == "close":
            logging.info("Issue close event detected — skipping.")
            return "", 204

        # ✅ 匹配触发关键词
        if triggered:
            logging.info("Trigger keyword detected, starting workflow...")

            # 发送任务开始评论
            start_comment = (
                f"🚀 **TestAgent 任务开始执行**\n\n"
                f"- 仓库: `{owner}/{repo_name}`\n"
                f"- Issue: #{issue_number}\n"
                f"- 开始时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
                f"正在分析代码并生成测试用例，请稍候..."
            )

            try:
                start_success = comment_on_issue(owner, repo_name, issue_number, start_comment, data)
                if start_success:
                    logging.info("✅ Start comment posted successfully")
                else:
                    logging.error("❌ Failed to post start comment")
            except Exception as e:
                logging.error(f"Failed to post start comment: {str(e)}")

    if not triggered:
        logging.info("No trigger keyword found — ignoring")
        return "", 204

    try:
        run_workflow_and_comment(owner, repo_name, issue_number, data)
    except Exception as e:
        logging.exception("Workflow failed")
        try:
            error_msg = f"❌ 工作流执行失败: {str(e)}"
            comment_on_issue(owner, repo_name, issue_number, error_msg, data)
        except:
            pass

    return "", 200


def run_workflow_and_comment(owner, repo_name, issue_number, data):
    # 检查Neo4j连接
    if not check_neo4j_connection():
        error_msg = "Neo4j数据库连接失败，请确保Neo4j服务正在运行并检查连接配置"
        logging.error(error_msg)
        try:
            comment_on_issue(owner, repo_name, issue_number, f"❌ {error_msg}", data)
        except:
            pass
        return

    repo = data.get("repository") or data.get("project", {})
    clone_url = (
            repo.get("clone_url")
            or repo.get("git_http_url")
            or repo.get("http_url")
            or repo.get("url")
    )
    if not clone_url:
        raise RuntimeError("Cannot find clone_url in payload")

    # create tmpdir
    tmpdir = tempfile.mkdtemp(prefix=f"{owner}_{repo_name}_", dir=WORK_DIR)
    logging.info("Cloning into %s", tmpdir)

    # embed PAT into https clone url
    if clone_url.startswith("https://") and GITCODE_PAT:
        auth_clone = clone_url.replace("https://", f"https://oauth2:{GITCODE_PAT}@")
    else:
        auth_clone = clone_url

    try:
        clone_result = subprocess.run(
            ["git", "clone", "--depth", "1", auth_clone, tmpdir],
            check=True,
            text=True,
            capture_output=True,
            timeout=600
        )
        logging.info(f"Clone completed")
    except subprocess.CalledProcessError as e:
        logging.error(f"git clone failed: {e.returncode}\nstdout: {e.stdout}\nstderr: {e.stderr}")
        raise RuntimeError("git clone failed: " + (e.stderr or str(e)))

    # prepare env for running cli.py
    env = os.environ.copy()
    env_vars = {
        "MAVEN_HOME": MAVEN_HOME,
        "JAVA_HOME": JAVA_HOME,
        "JAVA_VERSION": JAVA_VERSION,
        "NEO4J_URI": NEO4J_URI,
        "NEO4J_USER": NEO4J_USER,
        "NEO4J_PW": NEO4J_PW
    }
    for key, value in env_vars.items():
        if value:
            env[key] = value
            logging.info(f"Set env {key}")

    # 检查CLI路径
    if not os.path.exists(CLI_PATH):
        raise RuntimeError(f"cli.py not found at {CLI_PATH}")

    cmd = [sys.executable, CLI_PATH, "-p", tmpdir]
    logging.info("Running CLI: %s", " ".join(cmd))

    output = ""
    status = "未知"
    try:
        # 使用二进制模式读取，避免编码问题
        process = subprocess.Popen(
            cmd,
            cwd=AGENT_ROOT,
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT
        )

        # 读取二进制输出并处理编码
        while True:
            line_bytes = process.stdout.readline()
            if not line_bytes and process.poll() is not None:
                break
            if line_bytes:
                try:
                    # 尝试UTF-8解码
                    line = line_bytes.decode('utf-8').rstrip()
                except UnicodeDecodeError:
                    try:
                        # 尝试GBK解码（Windows中文环境）
                        line = line_bytes.decode('gbk').rstrip()
                    except UnicodeDecodeError:
                        # 最后尝试使用忽略错误的方式
                        line = line_bytes.decode('utf-8', errors='ignore').rstrip()

                logging.info(f"CLI: {line}")
                output += line + "\n"

        rc = process.poll()
        status = "成功" if rc == 0 else f"失败 (code {rc})"

    except subprocess.TimeoutExpired:
        output = "执行超时（3600秒）"
        status = "失败 (超时)"
        process.kill()
    except Exception as e:
        output = f"执行异常: {str(e)}"
        status = "失败 (Exception)"
        logging.exception("CLI execution failed")

    # 准备评论内容 - 限制总长度不超过8000字符
    max_total_chars = 7000  # 留一些余量

    # 读取测试用例文件内容（摘要）
    test_cases_count = 0
    test_case_path = os.path.join(AGENT_ROOT, "result", "log", "test_case.jsonl")

    try:
        if os.path.exists(test_case_path):
            with open(test_case_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()
                test_cases_count = len(lines)
    except Exception as e:
        logging.error(f"读取测试用例文件失败: {str(e)}")
        test_cases_count = 0

    # 创建PR包含完整的测试用例
    pr_info = None
    if status == "成功":
        pr_info = create_pr_with_test_cases(owner, repo_name, tmpdir, issue_number, data)

    # 构建基础评论内容（不包含详细日志）
    if pr_info:
        pr_section = (
            f"\n\n**📁 完整测试用例:**\n"
            f"- PR: {pr_info['url']}\n"
            f"- 文件路径: `{pr_info['test_case_path']}`\n"
            f"- 分支: `{pr_info['branch']}`"
        )
    else:
        pr_section = "\n\n**注意:** 测试用例文件已生成在本地，但未创建PR"

    base_comment = (
        f"TestAgent 执行结果：**{status}**\n\n"
        f"触发仓库：`{owner}/{repo_name}`\n\n"
        f"生成测试用例数量：**{test_cases_count}** 个"
        f"{pr_section}"
    )

    # 计算剩余可用字符数
    remaining_chars = max_total_chars - len(base_comment) - 100  # 留一些余量给日志标题

    # 准备执行日志摘要
    if remaining_chars > 100:  # 只有剩余足够空间时才添加日志
        # 提取关键信息（成功/错误消息）
        important_lines = []
        lines = output.split('\n')

        # 查找关键行（错误、异常、成功等关键词）
        keywords = ['error', 'exception', 'fail', 'success', '完成', '失败', '成功', '错误']
        for line in lines:
            if any(keyword in line.lower() for keyword in keywords):
                important_lines.append(line)

        # 如果关键行太少，添加开头和结尾的一些行
        if len(important_lines) < 5:
            important_lines = lines[:3] + important_lines + lines[-3:]

        # 去重并限制长度
        important_lines = list(dict.fromkeys(important_lines))  # 去重
        log_preview = "\n".join(important_lines[:20])  # 最多20行

        if len(log_preview) > remaining_chars:
            log_preview = log_preview[:remaining_chars] + "\n\n...（日志摘要被截断）"

        log_section = f"\n\n**关键日志摘要:**\n```\n{log_preview}\n```"
    else:
        log_section = "\n\n**日志过长，已省略。详细信息请查看PR中的测试用例文件。**"

    comment_body = base_comment + log_section

    # 最终检查长度
    if len(comment_body) > max_total_chars:
        # 如果仍然超长，进一步截断
        comment_body = comment_body[:max_total_chars - 100] + "\n\n...（评论内容被截断）"

    # 发送评论
    try:
        success = comment_on_issue(owner, repo_name, issue_number, comment_body, data)
        if success:
            logging.info("✅ Comment posted successfully")
        else:
            logging.error("❌ Failed to post comment")
    except Exception as e:
        logging.error(f"Failed to post comment: {str(e)}")

    # cleanup
    safe_rmtree(tmpdir)


if __name__ == "__main__":
    host = os.environ.get("HOST", "0.0.0.0")
    port = int(os.environ.get("PORT", "8000"))
    logging.info("Starting webhook server on %s:%s", host, port)
    app.run(host=host, port=port)